/*
 * Copyright (c) 2003-2006 jMonkeyEngine, 2007 Aleksey Nikiforov
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors 
 *   may be used to endorse or promote products derived from this software 
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.lex.game;


import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.ImageIcon;

import org.lex.input.StrategicHandler;
import org.lex.input.StrategicCameraEffects;
import org.lex.input.effects.HeightMap;
import org.lex.input.mouse.MouseBindingManager;
import org.lex.input.mouse.MouseManager;
import org.lex.input.mouse.component.CombinedMouse;

import com.jme.app.SimpleGame;
import com.jme.bounding.BoundingBox;
import com.jme.image.Texture;
import com.jme.input.FirstPersonHandler;
import com.jme.input.InputHandler;
import com.jme.input.KeyBindingManager;
import com.jme.input.KeyInput;
import com.jme.input.controls.binding.MouseButtonBinding;
import com.jme.light.DirectionalLight;
import com.jme.math.FastMath;
import com.jme.math.Quaternion;
import com.jme.math.Vector2f;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.renderer.Renderer;
import com.jme.scene.Controller;
import com.jme.scene.Node;
import com.jme.scene.Spatial;
import com.jme.scene.Text;
import com.jme.scene.Spatial.CullHint;
import com.jme.scene.Spatial.LightCombineMode;
import com.jme.scene.Spatial.TextureCombineMode;
import com.jme.scene.shape.Box;
import com.jme.scene.shape.Quad;
import com.jme.scene.state.BlendState;
import com.jme.scene.state.CullState;
import com.jme.scene.state.FogState;
import com.jme.scene.state.TextureState;
import com.jme.util.TextureManager;
import com.jmex.terrain.TerrainPage;
import com.jmex.terrain.util.FaultFractalHeightMap;
import com.jmex.terrain.util.ProceduralTextureGenerator;

public class Demo extends SimpleGame {

	private KeyBindingManager keyboard;
	private MouseBindingManager mouseBinding;
	private StrategicHandler strategicHandler;
	private FirstPersonHandler firstPersonHandler;
	private StrategicCameraEffects camEffects;
	private boolean filtersEnabled;
	
	private Text infoFilter;
	private Text infoFollow;
	private Text infoTimer;
	private StringBuffer sb = new StringBuffer();
	private TerrainPage terrain;
	
	private String defaultCursor = "defaultCursor";
	private String attackCursor = "attackCursor";
	private String selectCursor = "selectCursor";
	private String[] cursors = {defaultCursor, attackCursor, selectCursor};
	private int currentCursorIndex;
	
	private String cmdToggleFilters = "toggleFilters";
	private String cmdNextCursor = "nextCursor";
	private String cmdChangeLocation = "changeLocation";
	private String cmdFollowObject = "followObject";
	private String cmdFollowObjectRotation = "followObjectRotation";
	private String cmdFollowObjectAligned = "followObjectAligned";
	private String cmdToggleRunnerMove = "toggleRunnerMove";
	private String cmdToggleHeightOffset = "toggleHeightOffset";
	private String cmdToggleHandler = "toggleHandler";
	private String cmdFlyToOrigin = "flyToOrigin";
	
	private String mbLeftClick = "leftClick";
	
	private boolean viewLocked;
	private boolean followingRotation;
	private boolean runnerMoving = true;
	private Spatial runner;
	private Vector3f rDirection = new Vector3f();
	private float rSpeed = 50;
	private float tSpeed = 10;
	private Quaternion rTurn = new Quaternion();
	
	private float modifiedHeight = 50;
	
	private Quad minimap;
	private float minimapWidth = 200;
	private float minimapHeight = 200;
	private Vector2f mapLocation = new Vector2f();
	
	private float handlerTime;
	private DecimalFormat format = new DecimalFormat("0.00000");
	
	public static void main(String[] args) {
		Logger.getLogger("").setLevel(Level.WARNING);
		Demo game = new Demo();
		game.setConfigShowMode(ConfigShowMode.AlwaysShow);
		game.start();
	}
	
	@Override
	protected void simpleInitGame() {
		setupTerrain();
		setupFollowObject();
		setupControls();
		setupInput();
		setupMisc();
	}
	
	public void setupTerrain() {
		Node terrainNode = new Node();
		rootNode.attachChild(terrainNode);
		
		// this piece is pretty much copied from the jme terrain test
		
		FaultFractalHeightMap heightMap = new FaultFractalHeightMap(
				257, 32, 0, 255, 0.75f);
	    Vector3f terrainScale = new Vector3f(10,1,10);
	    heightMap.setHeightScale( 0.001f);
	    terrain = new TerrainPage("Terrain", 33,
	    		heightMap.getSize(), terrainScale,
	    		heightMap.getHeightMap());

	    terrain.setDetailTexture(1, 16);
	    terrainNode.attachChild(terrain);
	    
	    ProceduralTextureGenerator pt = new ProceduralTextureGenerator(heightMap);
	    pt.addTexture(new ImageIcon(Demo.class.getClassLoader().getResource(
	        "jmetest/data/texture/grassb.png")), -128, 0, 128);
	    pt.addTexture(new ImageIcon(Demo.class.getClassLoader().getResource(
	        "jmetest/data/texture/dirt.jpg")), 0, 128, 255);
	    pt.addTexture(new ImageIcon(Demo.class.getClassLoader().getResource(
	        "jmetest/data/texture/highest.jpg")), 128, 255, 384);

	    pt.createTexture(512);

	    TextureState ts = display.getRenderer().createTextureState();
	    ts.setEnabled(true);
	    Texture t1 = TextureManager.loadTexture(
	        pt.getImageIcon().getImage(),
	        Texture.MinificationFilter.Trilinear, 
	        Texture.MagnificationFilter.Bilinear,
	        true);
	    ts.setTexture(t1, 0);

	    Texture t2 = TextureManager.loadTexture(
    		Demo.class.getClassLoader().getResource("jmetest/data/texture/Detail.jpg"),
			Texture.MinificationFilter.Trilinear, 
	        Texture.MagnificationFilter.Bilinear);
	    
	    ts.setTexture(t2, 1);
	    t2.setWrap(Texture.WrapMode.Repeat);

	    t1.setApply(Texture.ApplyMode.Combine);
	    t1.setCombineFuncRGB(Texture.CombinerFunctionRGB.Modulate);
	    t1.setCombineSrc0RGB(Texture.CombinerSource.CurrentTexture);
	    t1.setCombineOp0RGB(Texture.CombinerOperandRGB.SourceColor);
	    t1.setCombineSrc1RGB(Texture.CombinerSource.PrimaryColor);
	    t1.setCombineOp1RGB(Texture.CombinerOperandRGB.SourceColor);

	    t2.setApply(Texture.ApplyMode.Combine);
        t2.setCombineFuncRGB(Texture.CombinerFunctionRGB.AddSigned);
        t2.setCombineSrc0RGB(Texture.CombinerSource.CurrentTexture);
        t2.setCombineOp0RGB(Texture.CombinerOperandRGB.SourceColor);
        t2.setCombineSrc1RGB(Texture.CombinerSource.Previous);
        t2.setCombineOp1RGB(Texture.CombinerOperandRGB.SourceColor);
        
	    terrainNode.setRenderState(ts);

	    FogState fs = display.getRenderer().createFogState();
	    fs.setDensity(0.0015f);
	    fs.setEnabled(true);
	    fs.setColor(new ColorRGBA(0.5f, 0.55f, 0.5f, 0.5f));
	    fs.setDensityFunction(FogState.DensityFunction.Exponential);
	    fs.setQuality(FogState.Quality.PerVertex);
	    terrainNode.setRenderState(fs);
	    
	    terrainNode.lock();
	    terrainNode.lockBranch();
	    
	    CullState cs = display.getRenderer().createCullState();
	    cs.setCullFace(CullState.Face.Back);
	    cs.setEnabled(true);
	    terrainNode.setRenderState(cs);
	    
	    lightState.detachAll();
	    
	    DirectionalLight dl = new DirectionalLight();
	    dl.setDiffuse(new ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
	    dl.setDirection(new Vector3f(1, -0.5f, 1));
	    dl.setEnabled(true);
	    lightState.attach(dl);
	    
	    DirectionalLight dr = new DirectionalLight();
	    dr.setEnabled(true);
	    dr.setDiffuse(new ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
	    dr.setAmbient(new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f));
	    dr.setDirection(new Vector3f(0.5f, -0.5f, 0).normalizeLocal());
	    lightState.attach(dr);
	}
	
    protected void cameraPerspective() {
        cam.setFrustumPerspective( 60.0f, (float) display.getWidth()
                / (float) display.getHeight(), 1, 1500 );
        cam.setParallelProjection( false );
        cam.update();
    }
	
	public void setupFollowObject() {
		runner = new Box("Runner", new Vector3f(), 10, 10, 20);
		runner.setModelBound(new BoundingBox());
		runner.updateModelBound();
		
		runner.addController(new Controller() {
			private static final long serialVersionUID = 1L;
			private Vector3f runnerDirection = Vector3f.UNIT_Z.negate();
			public void update(float time) {
				if (!runnerMoving) return;
				
				runner.getLocalRotation().mult(
						runnerDirection, rDirection);
				runner.getLocalTranslation().addLocal(
						rDirection.multLocal(rSpeed*time));
				
				rTurn.fromAngleNormalAxis(
						tSpeed*time*FastMath.DEG_TO_RAD, Vector3f.UNIT_Y);
				runner.getLocalRotation().multLocal(rTurn);
				
				Vector3f pos = runner.getLocalTranslation();
				float height = terrain.getHeight(pos);
				runner.setLocalTranslation(pos.x, height, pos.z);
			}
		});
		
		rootNode.attachChild(runner);
	}

	public void setupControls() {
		keyboard = KeyBindingManager.getKeyBindingManager();
		
		keyboard.add(cmdToggleFilters, KeyInput.KEY_F);
		keyboard.add(cmdNextCursor, KeyInput.KEY_C);
		keyboard.add(cmdChangeLocation, KeyInput.KEY_L);
		keyboard.add(cmdFollowObject, KeyInput.KEY_Z);
		keyboard.add(cmdFollowObjectRotation, KeyInput.KEY_X);
		keyboard.add(cmdFollowObjectAligned, KeyInput.KEY_V);
		keyboard.add(cmdToggleRunnerMove, KeyInput.KEY_SPACE);
		keyboard.add(cmdToggleHeightOffset, KeyInput.KEY_H);
		keyboard.add(cmdToggleHandler, KeyInput.KEY_TAB);
		keyboard.add(cmdFlyToOrigin, KeyInput.KEY_NUMPAD0);
		
		mouseBinding = MouseBindingManager.get();
		mouseBinding.addCommand(mbLeftClick, MouseButtonBinding.LEFT_BUTTON);
	}
	
	@SuppressWarnings("unchecked")
	public void setupInput() {
		firstPersonHandler = (FirstPersonHandler) input;
		firstPersonHandler.setEnabled(false);
		
		input = new InputHandler();
		input.addToAttachedHandlers(firstPersonHandler);
		
		MouseManager mouseManager = new MouseManager(CombinedMouse.get());
		mouseManager.registerWithInputHandler(input);
		statNode.attachChild(mouseManager.getMouse().getMouseSpatial());
		mouseManager.setMousePosition(
				display.getWidth()/2, display.getHeight()/2);
		
		mouseManager.setCursor(defaultCursor,
				"org/lex/cursors/goldenarrow_v2/default.cursor");
		mouseManager.setCursor(attackCursor,
				"org/lex/cursors/goldenarrow_v2/spinning.cursor");
		mouseManager.setCursor(selectCursor,
				"org/lex/cursors/goldenarrow_select/default.cursor");
		
		HeightMap heightMap = new HeightMap() {
			public float getHeightAt(Vector3f location) {
				float height = terrain.getHeight(location);
				if (Float.isNaN(height)) height = 0;
				return height;
			}
		};
		strategicHandler = new StrategicHandler(
				cam, mouseManager, heightMap);
		camEffects = new StrategicCameraEffects(strategicHandler);
		strategicHandler.setCameraEffects(camEffects);
		input.addToAttachedHandlers(strategicHandler);

		
		strategicHandler.updateProperties(
				StrategicHandlerDefaults.getCustomConfig());
		
		HashMap cursors = new HashMap();
		cursors.put(StrategicHandler.CURSOR_ROTATE,
				"org/lex/cursors/goldenarrow_rotate/default.cursor");
		cursors.put(StrategicHandler.CURSOR_SCROLL_UP,
				"org/lex/cursors/goldenscroll_up/default.cursor");
		cursors.put(StrategicHandler.CURSOR_SCROLL_UPLEFT,
		"org/lex/cursors/goldenscroll_upleft/default.cursor");
		strategicHandler.updateCursors(cursors);
		
		/*
		// this is how to set cursors via setters
			strategicHandler.getMouseManager().setCursor(
				StrategicHandler.CURSOR_ROTATE,
				Cursor.load(this.getClass().getClassLoader().getResource(
					"org/lex/cursors/goldenarrow.car"),
				"spinning.cursor"));
		*/
	}
	
	public void setupMisc() {
		Text info = Text.createDefaultTextLabel( "infoKeys1" );
		info.setTextureCombineMode( TextureCombineMode.Replace );
		info.setLocalTranslation(0, display.getHeight() - 40, 0);
		info.print("Press F to enable/disable input filters and " +
				"C to switch cursors.");
        statNode.attachChild(info);
		
        infoFilter = Text.createDefaultTextLabel( "InfoFilter" );
        infoFilter.setTextureCombineMode( TextureCombineMode.Replace );
        infoFilter.setLocalTranslation(0, display.getHeight() - 55, 0);
        filtersEnabled = true;
        infoFilter.print("Input Filters ENABLED");
        statNode.attachChild(infoFilter);
        
        info = Text.createDefaultTextLabel( "infoKeys2" );
		info.setTextureCombineMode( TextureCombineMode.Replace );
		info.setLocalTranslation(0, display.getHeight() - 85, 0);
		info.print("Z - followObject, X - followRotation, V - lockView and " +
				" SPACEBAR - move/stop.");
		statNode.attachChild(info);
        
        infoFollow = Text.createDefaultTextLabel( "infoFollow" );
        infoFollow.setTextureCombineMode( TextureCombineMode.Replace );
        infoFollow.setLocalTranslation(0, display.getHeight() - 100, 0);
        statNode.attachChild(infoFollow);
        
        infoTimer = Text.createDefaultTextLabel( "infoTimer" );
        infoTimer.setTextureCombineMode( TextureCombineMode.Replace );
        infoTimer.setLocalTranslation(0, display.getHeight() - 115, 0);
        statNode.attachChild(infoTimer);
        
		minimap = new Quad("myQuad", minimapWidth, minimapHeight);
		minimap.setLocalTranslation(minimapWidth/2 + 5, minimapHeight/2 + 20, 0);
        minimap.setRenderQueueMode(Renderer.QUEUE_ORTHO);
        minimap.setCullHint(CullHint.Never);
        minimap.setLightCombineMode(LightCombineMode.Off);
        minimap.setTextureCombineMode(Spatial.TextureCombineMode.Off);
        minimap.setDefaultColor(new ColorRGBA(ColorRGBA.white));
        minimap.getDefaultColor().a = 0.5f;
        BlendState as = display.getRenderer().createBlendState();
        as.setBlendEnabled(true);
		as.setSourceFunction(BlendState.SourceFunction.SourceAlpha);
		as.setDestinationFunction(BlendState.DestinationFunction.OneMinusSourceAlpha);
		as.setTestEnabled(true);
		as.setTestFunction(BlendState.TestFunction.GreaterThan);
		as.setEnabled(true);
		minimap.setRenderState(as);
        statNode.attachChild(minimap);
        
        info = Text.createDefaultTextLabel( "infoKeys3" );
		info.setTextureCombineMode( TextureCombineMode.Replace );
		info.setLocalTranslation(minimapWidth + 10, 60, 0);
		info.print("Left-click on the minimap to fly to location,");
        statNode.attachChild(info);
        
        info = Text.createDefaultTextLabel( "infoKeys4" );
		info.setTextureCombineMode( TextureCombineMode.Replace );
		info.setLocalTranslation(minimapWidth + 10, 45, 0);
		info.print("TAB to toggle fps contoller on/off,");
        statNode.attachChild(info);
        
        info = Text.createDefaultTextLabel( "infoKeys5" );
		info.setTextureCombineMode( TextureCombineMode.Replace );
		info.setLocalTranslation(minimapWidth + 10, 30, 0);
		info.print("H - height offset and L or NUMPAD_0 - go to origin.");
        statNode.attachChild(info);
	}
	
	@Override
	protected void simpleUpdate() {
		
		if (strategicHandler.isEnabled()) {
			/*
			 * Remember to update your cursor every frame. You will want to
			 * do this, because after you do mouse picking you can choose
			 * differet cursors based on the result of the mouse pick.
			 * 
			 * Example: you can set a different cursor when the mouse is
			 * hovering over an enemy vs the default cursor.
			*/
			strategicHandler.useCursor(cursors[currentCursorIndex]);
			
		}
		
		sb.delete(0, sb.length());
		sb.append("FollowObject = ");
		sb.append(strategicHandler.getFollowObject() != null ? "ON" : "OFF");
		sb.append(". FollowRotaion = ");
		sb.append(followingRotation ? "ON" : "OFF");
		sb.append(". LockView = ");
		sb.append(viewLocked ? "ON" : "OFF");
		sb.append(".");
		infoFollow.print(sb);
		
		sb.delete(0, sb.length());
		sb.append("Handler update took = ");
		sb.append(format.format(handlerTime));
		sb.append("seconds.");
		infoTimer.print(sb);
		
	}
	
	@Override
	protected void updateInput() {
		long start = System.nanoTime();
		super.updateInput();
		handlerTime = (System.nanoTime() - start)*10e-9f;
		
		if (keyboard.isValidCommand(cmdToggleFilters, false)) {
            filtersEnabled = !filtersEnabled;
            
            if (filtersEnabled) {
            	strategicHandler.updateProperties(
            			StrategicHandlerDefaults.getFilterConfig());
            	infoFilter.print("Input Filters ENABLED");
            } else {
            	strategicHandler.updateProperties(
            			StrategicHandlerDefaults.getNoFilterConfig());
            	infoFilter.print("Input Filters DISABLED");
            }
        }
		if (keyboard.isValidCommand(cmdNextCursor, false)){
			currentCursorIndex++;
			if (currentCursorIndex >= cursors.length) currentCursorIndex = 0;
		}
		if (keyboard.isValidCommand(cmdChangeLocation, false)){
			strategicHandler.setWorldTranslation(0, 0, 0);
		}
		if (keyboard.isValidCommand(cmdToggleHeightOffset, false)){
			float height = strategicHandler.getScrollingPlane().getHeight();
			if (height == modifiedHeight) height = 0;
			else height = modifiedHeight;
			
			strategicHandler.getScrollingPlane().setHeight(height);
		}
		if (keyboard.isValidCommand(cmdToggleRunnerMove, false)){
			runnerMoving = !runnerMoving;
		}
		
		boolean followObject = (strategicHandler.getFollowObject() != null);
		if (keyboard.isValidCommand(cmdFollowObject, false)){
			if (followingRotation || viewLocked) {
				followObject = true;
			} else {
				followObject = !followObject;
			}
			
			followingRotation = false;
			viewLocked = false;
			
			strategicHandler.setHorizontalRotationEnabled(!viewLocked);
			strategicHandler.setFollowObject(followObject ? runner : null,
					   followingRotation, viewLocked, 0);
			
		} else if (keyboard.isValidCommand(cmdFollowObjectRotation, false)){
			if (!followingRotation || viewLocked) {
				followObject = true;
			} else {
				followObject = !followObject;
			}
			
			if (followObject) {
				followingRotation = true;
				viewLocked = false;
			} else {
				followingRotation = false;
				viewLocked = false;
			}
			
			strategicHandler.setHorizontalRotationEnabled(!viewLocked);
			strategicHandler.setFollowObject(followObject ? runner : null,
					   followingRotation, viewLocked, 0);
			
		} else if (keyboard.isValidCommand(cmdFollowObjectAligned, false)){
			if (!viewLocked) {
				followObject = true;
			} else {
				followObject = !followObject;
			}
			
			if (followObject) {
				followingRotation = true;
				viewLocked = true;
			} else {
				followingRotation = false;
				viewLocked = false;
			}
			
			strategicHandler.setFollowObject(followObject ? runner : null,
					   followingRotation, viewLocked, 0);
			strategicHandler.setHorizontalRotationEnabled(!viewLocked);
		}
		
		if (keyboard.isValidCommand(cmdToggleHandler, false)) {
			if (strategicHandler.isEnabled()) {
				strategicHandler.setEnabled(false);
				strategicHandler.clearFilterEffects();
				strategicHandler.getMouseManager().getMouse().disable();
				firstPersonHandler.setEnabled(true);
			} else {
				firstPersonHandler.setEnabled(false);
				strategicHandler.setEnabled(true);
				strategicHandler.getMouseManager().getMouse().enable();
				camEffects.takeOverCamera();
			}
		}
		
		if (keyboard.isValidCommand(cmdFlyToOrigin, false)) {
			if (!strategicHandler.isEnabled()) {
				firstPersonHandler.setEnabled(false);
				strategicHandler.setEnabled(true);
				strategicHandler.getMouseManager().getMouse().enable();
			}
			camEffects.flyToLocation(Vector3f.ZERO);
		}
		
		if (mouseBinding.isValidCommand(mbLeftClick, false)) {
			Vector2f mousePosition =
				strategicHandler.getMouseManager().getMousePosition();
			
			mapLocation.x = (mousePosition.x - minimap.getLocalTranslation().x
						+ minimapWidth/2)/minimapWidth;
			mapLocation.y = (mousePosition.y - minimap.getLocalTranslation().y
						+ minimapHeight/2)/minimapHeight;
			
			if (mapLocation.x >= 0 && mapLocation.x <= 1
					&& mapLocation.y >= 0 && mapLocation.y <= 1)
			{
				mapLocation.x = mapLocation.x*2500 - 1250;
				mapLocation.y = mapLocation.y*2500 - 1250;
				
				if (!strategicHandler.isEnabled()) {
					firstPersonHandler.setEnabled(false);
					strategicHandler.setEnabled(true);
					strategicHandler.getMouseManager().getMouse().enable();
				}
				camEffects.flyToLocation(mapLocation);
			}
		}
	}
	
}
